/*
 * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.jmx.mbeanserver;

import static com.sun.jmx.mbeanserver.Util.*;
import static com.sun.jmx.mbeanserver.MXBeanIntrospector.typeName;

import static javax.management.openmbean.SimpleType.*;

import com.sun.jmx.remote.util.EnvHelp;

import java.io.InvalidObjectException;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.WeakHashMap;

import javax.management.JMX;
import javax.management.ObjectName;
import javax.management.openmbean.ArrayType;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataInvocationHandler;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeDataView;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;
import sun.reflect.misc.MethodUtil;
import sun.reflect.misc.ReflectUtil;

/**
 * <p>A converter between Java types and the limited set of classes
 * defined by Open MBeans.</p>
 *
 * <p>A Java type is an instance of java.lang.reflect.Type.  For our
 * purposes, it is either a Class, such as String.class or int.class;
 * or a ParameterizedType, such as List<String> or Map<Integer,
 * String[]>.  On J2SE 1.4 and earlier, it can only be a Class.</p>
 *
 * <p>Each Type is associated with an DefaultMXBeanMappingFactory.  The
 * DefaultMXBeanMappingFactory defines an OpenType corresponding to the Type, plus a
 * Java class corresponding to the OpenType.  For example:</p>
 *
 * <pre>
 *   Type                     Open class     OpenType
 *   ----                     ----------     --------
 *   Integer                Integer        SimpleType.INTEGER
 *   int                            int            SimpleType.INTEGER
 *   Integer[]              Integer[]      ArrayType(1, SimpleType.INTEGER)
 *   int[]                  Integer[]      ArrayType(SimpleType.INTEGER, true)
 *   String[][]             String[][]     ArrayType(2, SimpleType.STRING)
 *   List<String>                   String[]       ArrayType(1, SimpleType.STRING)
 *   ThreadState (an Enum)    String         SimpleType.STRING
 *   Map<Integer, String[]>   TabularData          TabularType(
 *                                           CompositeType(
 *                                             {"key", SimpleType.INTEGER},
 *                                             {"value",
 *                                               ArrayType(1,
 *                                                SimpleType.STRING)}),
 *                                           indexNames={"key"})
 *   </pre>
 *
 * <p>Apart from simple types, arrays, and collections, Java types are
 * converted through introspection into CompositeType.  The Java type
 * must have at least one getter (method such as "int getSize()" or
 * "boolean isBig()"), and we must be able to deduce how to
 * reconstruct an instance of the Java class from the values of the
 * getters using one of various heuristics.</p>
 *
 * @since 1.6
 */
public class DefaultMXBeanMappingFactory extends MXBeanMappingFactory {

  static abstract class NonNullMXBeanMapping extends MXBeanMapping {

    NonNullMXBeanMapping(Type javaType, OpenType<?> openType) {
      super(javaType, openType);
    }

    @Override
    public final Object fromOpenValue(Object openValue)
        throws InvalidObjectException {
      if (openValue == null) {
        return null;
      } else {
        return fromNonNullOpenValue(openValue);
      }
    }

    @Override
    public final Object toOpenValue(Object javaValue) throws OpenDataException {
      if (javaValue == null) {
        return null;
      } else {
        return toNonNullOpenValue(javaValue);
      }
    }

    abstract Object fromNonNullOpenValue(Object openValue)
        throws InvalidObjectException;

    abstract Object toNonNullOpenValue(Object javaValue)
        throws OpenDataException;

    /**
     * <p>True if and only if this MXBeanMapping's toOpenValue and
     * fromOpenValue methods are the identity function.</p>
     */
    boolean isIdentity() {
      return false;
    }
  }

  static boolean isIdentity(MXBeanMapping mapping) {
    return (mapping instanceof NonNullMXBeanMapping &&
        ((NonNullMXBeanMapping) mapping).isIdentity());
  }

  private static final class Mappings
      extends WeakHashMap<Type, WeakReference<MXBeanMapping>> {

  }

  private static final Mappings mappings = new Mappings();

  /**
   * Following List simply serves to keep a reference to predefined
   * MXBeanMappings so they don't get garbage collected.
   */
  private static final List<MXBeanMapping> permanentMappings = newList();

  private static synchronized MXBeanMapping getMapping(Type type) {
    WeakReference<MXBeanMapping> wr = mappings.get(type);
    return (wr == null) ? null : wr.get();
  }

  private static synchronized void putMapping(Type type, MXBeanMapping mapping) {
    WeakReference<MXBeanMapping> wr =
        new WeakReference<MXBeanMapping>(mapping);
    mappings.put(type, wr);
  }

  private static synchronized void putPermanentMapping(
      Type type, MXBeanMapping mapping) {
    putMapping(type, mapping);
    permanentMappings.add(mapping);
  }

  static {
        /* Set up the mappings for Java types that map to SimpleType.  */

    final OpenType<?>[] simpleTypes = {
        BIGDECIMAL, BIGINTEGER, BOOLEAN, BYTE, CHARACTER, DATE,
        DOUBLE, FLOAT, INTEGER, LONG, OBJECTNAME, SHORT, STRING,
        VOID,
    };

    for (int i = 0; i < simpleTypes.length; i++) {
      final OpenType<?> t = simpleTypes[i];
      Class<?> c;
      try {
        c = Class.forName(t.getClassName(), false,
            ObjectName.class.getClassLoader());
      } catch (ClassNotFoundException e) {
        // the classes that these predefined types declare must exist!
        throw new Error(e);
      }
      final MXBeanMapping mapping = new IdentityMapping(c, t);
      putPermanentMapping(c, mapping);

      if (c.getName().startsWith("java.lang.")) {
        try {
          final Field typeField = c.getField("TYPE");
          final Class<?> primitiveType = (Class<?>) typeField.get(null);
          final MXBeanMapping primitiveMapping =
              new IdentityMapping(primitiveType, t);
          putPermanentMapping(primitiveType, primitiveMapping);
          if (primitiveType != void.class) {
            final Class<?> primitiveArrayType =
                Array.newInstance(primitiveType, 0).getClass();
            final OpenType<?> primitiveArrayOpenType =
                ArrayType.getPrimitiveArrayType(primitiveArrayType);
            final MXBeanMapping primitiveArrayMapping =
                new IdentityMapping(primitiveArrayType,
                    primitiveArrayOpenType);
            putPermanentMapping(primitiveArrayType,
                primitiveArrayMapping);
          }
        } catch (NoSuchFieldException e) {
          // OK: must not be a primitive wrapper
        } catch (IllegalAccessException e) {
          // Should not reach here
          assert (false);
        }
      }
    }
  }

  /**
   * Get the converter for the given Java type, creating it if necessary.
   */
  @Override
  public synchronized MXBeanMapping mappingForType(Type objType,
      MXBeanMappingFactory factory)
      throws OpenDataException {
    if (inProgress.containsKey(objType)) {
      throw new OpenDataException(
          "Recursive data structure, including " + typeName(objType));
    }

    MXBeanMapping mapping;

    mapping = getMapping(objType);
    if (mapping != null) {
      return mapping;
    }

    inProgress.put(objType, objType);
    try {
      mapping = makeMapping(objType, factory);
    } catch (OpenDataException e) {
      throw openDataException("Cannot convert type: " + typeName(objType), e);
    } finally {
      inProgress.remove(objType);
    }

    putMapping(objType, mapping);
    return mapping;
  }

  private MXBeanMapping makeMapping(Type objType, MXBeanMappingFactory factory)
      throws OpenDataException {

        /* It's not yet worth formalizing these tests by having for example
           an array of factory classes, each of which says whether it
           recognizes the Type (Chain of Responsibility pattern).  */
    if (objType instanceof GenericArrayType) {
      Type componentType =
          ((GenericArrayType) objType).getGenericComponentType();
      return makeArrayOrCollectionMapping(objType, componentType, factory);
    } else if (objType instanceof Class<?>) {
      Class<?> objClass = (Class<?>) objType;
      if (objClass.isEnum()) {
        // Huge hack to avoid compiler warnings here.  The ElementType
        // parameter is ignored but allows us to obtain a type variable
        // T that matches <T extends Enum<T>>.
        return makeEnumMapping((Class<?>) objClass, ElementType.class);
      } else if (objClass.isArray()) {
        Type componentType = objClass.getComponentType();
        return makeArrayOrCollectionMapping(objClass, componentType,
            factory);
      } else if (JMX.isMXBeanInterface(objClass)) {
        return makeMXBeanRefMapping(objClass);
      } else {
        return makeCompositeMapping(objClass, factory);
      }
    } else if (objType instanceof ParameterizedType) {
      return makeParameterizedTypeMapping((ParameterizedType) objType,
          factory);
    } else {
      throw new OpenDataException("Cannot map type: " + objType);
    }
  }

  private static <T extends Enum<T>> MXBeanMapping
  makeEnumMapping(Class<?> enumClass, Class<T> fake) {
    ReflectUtil.checkPackageAccess(enumClass);
    return new EnumMapping<T>(Util.<Class<T>>cast(enumClass));
  }

  /* Make the converter for an array type, or a collection such as
   * List<String> or Set<Integer>.  We never see one-dimensional
   * primitive arrays (e.g. int[]) here because they use the identity
   * converter and are registered as such in the static initializer.
   */
  private MXBeanMapping
  makeArrayOrCollectionMapping(Type collectionType, Type elementType,
      MXBeanMappingFactory factory)
      throws OpenDataException {

    final MXBeanMapping elementMapping = factory.mappingForType(elementType, factory);
    final OpenType<?> elementOpenType = elementMapping.getOpenType();
    final ArrayType<?> openType = ArrayType.getArrayType(elementOpenType);
    final Class<?> elementOpenClass = elementMapping.getOpenClass();

    final Class<?> openArrayClass;
    final String openArrayClassName;
    if (elementOpenClass.isArray()) {
      openArrayClassName = "[" + elementOpenClass.getName();
    } else {
      openArrayClassName = "[L" + elementOpenClass.getName() + ";";
    }
    try {
      openArrayClass = Class.forName(openArrayClassName);
    } catch (ClassNotFoundException e) {
      throw openDataException("Cannot obtain array class", e);
    }

    if (collectionType instanceof ParameterizedType) {
      return new CollectionMapping(collectionType,
          openType, openArrayClass,
          elementMapping);
    } else {
      if (isIdentity(elementMapping)) {
        return new IdentityMapping(collectionType,
            openType);
      } else {
        return new ArrayMapping(collectionType,
            openType,
            openArrayClass,
            elementMapping);
      }
    }
  }

  private static final String[] keyArray = {"key"};
  private static final String[] keyValueArray = {"key", "value"};

  private MXBeanMapping
  makeTabularMapping(Type objType, boolean sortedMap,
      Type keyType, Type valueType,
      MXBeanMappingFactory factory)
      throws OpenDataException {

    final String objTypeName = typeName(objType);
    final MXBeanMapping keyMapping = factory.mappingForType(keyType, factory);
    final MXBeanMapping valueMapping = factory.mappingForType(valueType, factory);
    final OpenType<?> keyOpenType = keyMapping.getOpenType();
    final OpenType<?> valueOpenType = valueMapping.getOpenType();
    final CompositeType rowType =
        new CompositeType(objTypeName,
            objTypeName,
            keyValueArray,
            keyValueArray,
            new OpenType<?>[]{keyOpenType, valueOpenType});
    final TabularType tabularType =
        new TabularType(objTypeName, objTypeName, rowType, keyArray);
    return new TabularMapping(objType, sortedMap, tabularType,
        keyMapping, valueMapping);
  }

  /* We know how to translate List<E>, Set<E>, SortedSet<E>,
     Map<K,V>, SortedMap<K,V>, and that's it.  We don't accept
     subtypes of those because we wouldn't know how to deserialize
     them.  We don't accept Queue<E> because it is unlikely people
     would use that as a parameter or return type in an MBean.  */
  private MXBeanMapping
  makeParameterizedTypeMapping(ParameterizedType objType,
      MXBeanMappingFactory factory)
      throws OpenDataException {

    final Type rawType = objType.getRawType();

    if (rawType instanceof Class<?>) {
      Class<?> c = (Class<?>) rawType;
      if (c == List.class || c == Set.class || c == SortedSet.class) {
        Type[] actuals = objType.getActualTypeArguments();
        assert (actuals.length == 1);
        if (c == SortedSet.class) {
          mustBeComparable(c, actuals[0]);
        }
        return makeArrayOrCollectionMapping(objType, actuals[0], factory);
      } else {
        boolean sortedMap = (c == SortedMap.class);
        if (c == Map.class || sortedMap) {
          Type[] actuals = objType.getActualTypeArguments();
          assert (actuals.length == 2);
          if (sortedMap) {
            mustBeComparable(c, actuals[0]);
          }
          return makeTabularMapping(objType, sortedMap,
              actuals[0], actuals[1], factory);
        }
      }
    }
    throw new OpenDataException("Cannot convert type: " + objType);
  }

  private static MXBeanMapping makeMXBeanRefMapping(Type t)
      throws OpenDataException {
    return new MXBeanRefMapping(t);
  }

  private MXBeanMapping makeCompositeMapping(Class<?> c,
      MXBeanMappingFactory factory)
      throws OpenDataException {

    // For historical reasons GcInfo implements CompositeData but we
    // shouldn't count its CompositeData.getCompositeType() field as
    // an item in the computed CompositeType.
    final boolean gcInfoHack =
        (c.getName().equals("com.sun.management.GcInfo") &&
            c.getClassLoader() == null);

    ReflectUtil.checkPackageAccess(c);
    final List<Method> methods =
        MBeanAnalyzer.eliminateCovariantMethods(Arrays.asList(c.getMethods()));
    final SortedMap<String, Method> getterMap = newSortedMap();

        /* Select public methods that look like "T getX()" or "boolean
           isX()", where T is not void and X is not the empty
           string.  Exclude "Class getClass()" inherited from Object.  */
    for (Method method : methods) {
      final String propertyName = propertyName(method);

      if (propertyName == null) {
        continue;
      }
      if (gcInfoHack && propertyName.equals("CompositeType")) {
        continue;
      }

      Method old =
          getterMap.put(decapitalize(propertyName),
              method);
      if (old != null) {
        final String msg =
            "Class " + c.getName() + " has method name clash: " +
                old.getName() + ", " + method.getName();
        throw new OpenDataException(msg);
      }
    }

    final int nitems = getterMap.size();

    if (nitems == 0) {
      throw new OpenDataException("Can't map " + c.getName() +
          " to an open data type");
    }

    final Method[] getters = new Method[nitems];
    final String[] itemNames = new String[nitems];
    final OpenType<?>[] openTypes = new OpenType<?>[nitems];
    int i = 0;
    for (Map.Entry<String, Method> entry : getterMap.entrySet()) {
      itemNames[i] = entry.getKey();
      final Method getter = entry.getValue();
      getters[i] = getter;
      final Type retType = getter.getGenericReturnType();
      openTypes[i] = factory.mappingForType(retType, factory).getOpenType();
      i++;
    }

    CompositeType compositeType =
        new CompositeType(c.getName(),
            c.getName(),
            itemNames, // field names
            itemNames, // field descriptions
            openTypes);

    return new CompositeMapping(c,
        compositeType,
        itemNames,
        getters,
        factory);
  }

  /* Converter for classes where the open data is identical to the
     original data.  This is true for any of the SimpleType types,
     and for an any-dimension array of those.  It is also true for
     primitive types as of JMX 1.3, since an int[]
     can be directly represented by an ArrayType, and an int needs no mapping
     because reflection takes care of it.  */
  private static final class IdentityMapping extends NonNullMXBeanMapping {

    IdentityMapping(Type targetType, OpenType<?> openType) {
      super(targetType, openType);
    }

    boolean isIdentity() {
      return true;
    }

    @Override
    Object fromNonNullOpenValue(Object openValue)
        throws InvalidObjectException {
      return openValue;
    }

    @Override
    Object toNonNullOpenValue(Object javaValue) throws OpenDataException {
      return javaValue;
    }
  }

  private static final class EnumMapping<T extends Enum<T>>
      extends NonNullMXBeanMapping {

    EnumMapping(Class<T> enumClass) {
      super(enumClass, SimpleType.STRING);
      this.enumClass = enumClass;
    }

    @Override
    final Object toNonNullOpenValue(Object value) {
      return ((Enum<?>) value).name();
    }

    @Override
    final T fromNonNullOpenValue(Object value)
        throws InvalidObjectException {
      try {
        return Enum.valueOf(enumClass, (String) value);
      } catch (Exception e) {
        throw invalidObjectException("Cannot convert to enum: " +
            value, e);
      }
    }

    private final Class<T> enumClass;
  }

  private static final class ArrayMapping extends NonNullMXBeanMapping {

    ArrayMapping(Type targetType,
        ArrayType<?> openArrayType, Class<?> openArrayClass,
        MXBeanMapping elementMapping) {
      super(targetType, openArrayType);
      this.elementMapping = elementMapping;
    }

    @Override
    final Object toNonNullOpenValue(Object value)
        throws OpenDataException {
      Object[] valueArray = (Object[]) value;
      final int len = valueArray.length;
      final Object[] openArray = (Object[])
          Array.newInstance(getOpenClass().getComponentType(), len);
      for (int i = 0; i < len; i++) {
        openArray[i] = elementMapping.toOpenValue(valueArray[i]);
      }
      return openArray;
    }

    @Override
    final Object fromNonNullOpenValue(Object openValue)
        throws InvalidObjectException {
      final Object[] openArray = (Object[]) openValue;
      final Type javaType = getJavaType();
      final Object[] valueArray;
      final Type componentType;
      if (javaType instanceof GenericArrayType) {
        componentType =
            ((GenericArrayType) javaType).getGenericComponentType();
      } else if (javaType instanceof Class<?> &&
          ((Class<?>) javaType).isArray()) {
        componentType = ((Class<?>) javaType).getComponentType();
      } else {
        throw new IllegalArgumentException("Not an array: " +
            javaType);
      }
      valueArray = (Object[]) Array.newInstance((Class<?>) componentType,
          openArray.length);
      for (int i = 0; i < openArray.length; i++) {
        valueArray[i] = elementMapping.fromOpenValue(openArray[i]);
      }
      return valueArray;
    }

    public void checkReconstructible() throws InvalidObjectException {
      elementMapping.checkReconstructible();
    }

    /**
     * DefaultMXBeanMappingFactory for the elements of this array.  If this is an
     * array of arrays, the converter converts the second-level arrays,
     * not the deepest elements.
     */
    private final MXBeanMapping elementMapping;
  }

  private static final class CollectionMapping extends NonNullMXBeanMapping {

    CollectionMapping(Type targetType,
        ArrayType<?> openArrayType,
        Class<?> openArrayClass,
        MXBeanMapping elementMapping) {
      super(targetType, openArrayType);
      this.elementMapping = elementMapping;

            /* Determine the concrete class to be used when converting
               back to this Java type.  We convert all Lists to ArrayList
               and all Sets to TreeSet.  (TreeSet because it is a SortedSet,
               so works for both Set and SortedSet.)  */
      Type raw = ((ParameterizedType) targetType).getRawType();
      Class<?> c = (Class<?>) raw;
      final Class<?> collC;
      if (c == List.class) {
        collC = ArrayList.class;
      } else if (c == Set.class) {
        collC = HashSet.class;
      } else if (c == SortedSet.class) {
        collC = TreeSet.class;
      } else { // can't happen
        assert (false);
        collC = null;
      }
      collectionClass = Util.cast(collC);
    }

    @Override
    final Object toNonNullOpenValue(Object value)
        throws OpenDataException {
      final Collection<?> valueCollection = (Collection<?>) value;
      if (valueCollection instanceof SortedSet<?>) {
        Comparator<?> comparator =
            ((SortedSet<?>) valueCollection).comparator();
        if (comparator != null) {
          final String msg =
              "Cannot convert SortedSet with non-null comparator: " +
                  comparator;
          throw openDataException(msg, new IllegalArgumentException(msg));
        }
      }
      final Object[] openArray = (Object[])
          Array.newInstance(getOpenClass().getComponentType(),
              valueCollection.size());
      int i = 0;
      for (Object o : valueCollection) {
        openArray[i++] = elementMapping.toOpenValue(o);
      }
      return openArray;
    }

    @Override
    final Object fromNonNullOpenValue(Object openValue)
        throws InvalidObjectException {
      final Object[] openArray = (Object[]) openValue;
      final Collection<Object> valueCollection;
      try {
        valueCollection = cast(collectionClass.newInstance());
      } catch (Exception e) {
        throw invalidObjectException("Cannot create collection", e);
      }
      for (Object o : openArray) {
        Object value = elementMapping.fromOpenValue(o);
        if (!valueCollection.add(value)) {
          final String msg =
              "Could not add " + o + " to " +
                  collectionClass.getName() +
                  " (duplicate set element?)";
          throw new InvalidObjectException(msg);
        }
      }
      return valueCollection;
    }

    public void checkReconstructible() throws InvalidObjectException {
      elementMapping.checkReconstructible();
    }

    private final Class<? extends Collection<?>> collectionClass;
    private final MXBeanMapping elementMapping;
  }

  private static final class MXBeanRefMapping extends NonNullMXBeanMapping {

    MXBeanRefMapping(Type intf) {
      super(intf, SimpleType.OBJECTNAME);
    }

    @Override
    final Object toNonNullOpenValue(Object javaValue)
        throws OpenDataException {
      MXBeanLookup lookup = lookupNotNull(OpenDataException.class);
      ObjectName name = lookup.mxbeanToObjectName(javaValue);
      if (name == null) {
        throw new OpenDataException("No name for object: " + javaValue);
      }
      return name;
    }

    @Override
    final Object fromNonNullOpenValue(Object openValue)
        throws InvalidObjectException {
      MXBeanLookup lookup = lookupNotNull(InvalidObjectException.class);
      ObjectName name = (ObjectName) openValue;
      Object mxbean =
          lookup.objectNameToMXBean(name, (Class<?>) getJavaType());
      if (mxbean == null) {
        final String msg =
            "No MXBean for name: " + name;
        throw new InvalidObjectException(msg);
      }
      return mxbean;
    }

    private <T extends Exception> MXBeanLookup
    lookupNotNull(Class<T> excClass)
        throws T {
      MXBeanLookup lookup = MXBeanLookup.getLookup();
      if (lookup == null) {
        final String msg =
            "Cannot convert MXBean interface in this context";
        T exc;
        try {
          Constructor<T> con = excClass.getConstructor(String.class);
          exc = con.newInstance(msg);
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
        throw exc;
      }
      return lookup;
    }
  }

  private static final class TabularMapping extends NonNullMXBeanMapping {

    TabularMapping(Type targetType,
        boolean sortedMap,
        TabularType tabularType,
        MXBeanMapping keyConverter,
        MXBeanMapping valueConverter) {
      super(targetType, tabularType);
      this.sortedMap = sortedMap;
      this.keyMapping = keyConverter;
      this.valueMapping = valueConverter;
    }

    @Override
    final Object toNonNullOpenValue(Object value) throws OpenDataException {
      final Map<Object, Object> valueMap = cast(value);
      if (valueMap instanceof SortedMap<?, ?>) {
        Comparator<?> comparator = ((SortedMap<?, ?>) valueMap).comparator();
        if (comparator != null) {
          final String msg =
              "Cannot convert SortedMap with non-null comparator: " +
                  comparator;
          throw openDataException(msg, new IllegalArgumentException(msg));
        }
      }
      final TabularType tabularType = (TabularType) getOpenType();
      final TabularData table = new TabularDataSupport(tabularType);
      final CompositeType rowType = tabularType.getRowType();
      for (Map.Entry<Object, Object> entry : valueMap.entrySet()) {
        final Object openKey = keyMapping.toOpenValue(entry.getKey());
        final Object openValue = valueMapping.toOpenValue(entry.getValue());
        final CompositeData row;
        row =
            new CompositeDataSupport(rowType, keyValueArray,
                new Object[]{openKey,
                    openValue});
        table.put(row);
      }
      return table;
    }

    @Override
    final Object fromNonNullOpenValue(Object openValue)
        throws InvalidObjectException {
      final TabularData table = (TabularData) openValue;
      final Collection<CompositeData> rows = cast(table.values());
      final Map<Object, Object> valueMap =
          sortedMap ? newSortedMap() : newInsertionOrderMap();
      for (CompositeData row : rows) {
        final Object key =
            keyMapping.fromOpenValue(row.get("key"));
        final Object value =
            valueMapping.fromOpenValue(row.get("value"));
        if (valueMap.put(key, value) != null) {
          final String msg =
              "Duplicate entry in TabularData: key=" + key;
          throw new InvalidObjectException(msg);
        }
      }
      return valueMap;
    }

    @Override
    public void checkReconstructible() throws InvalidObjectException {
      keyMapping.checkReconstructible();
      valueMapping.checkReconstructible();
    }

    private final boolean sortedMap;
    private final MXBeanMapping keyMapping;
    private final MXBeanMapping valueMapping;
  }

  private final class CompositeMapping extends NonNullMXBeanMapping {

    CompositeMapping(Class<?> targetClass,
        CompositeType compositeType,
        String[] itemNames,
        Method[] getters,
        MXBeanMappingFactory factory) throws OpenDataException {
      super(targetClass, compositeType);

      assert (itemNames.length == getters.length);

      this.itemNames = itemNames;
      this.getters = getters;
      this.getterMappings = new MXBeanMapping[getters.length];
      for (int i = 0; i < getters.length; i++) {
        Type retType = getters[i].getGenericReturnType();
        getterMappings[i] = factory.mappingForType(retType, factory);
      }
    }

    @Override
    final Object toNonNullOpenValue(Object value)
        throws OpenDataException {
      CompositeType ct = (CompositeType) getOpenType();
      if (value instanceof CompositeDataView) {
        return ((CompositeDataView) value).toCompositeData(ct);
      }
      if (value == null) {
        return null;
      }

      Object[] values = new Object[getters.length];
      for (int i = 0; i < getters.length; i++) {
        try {
          Object got = MethodUtil.invoke(getters[i], value, (Object[]) null);
          values[i] = getterMappings[i].toOpenValue(got);
        } catch (Exception e) {
          throw openDataException("Error calling getter for " +
              itemNames[i] + ": " + e, e);
        }
      }
      return new CompositeDataSupport(ct, itemNames, values);
    }

    /**
     * Determine how to convert back from the CompositeData into
     * the original Java type.  For a type that is not reconstructible,
     * this method will fail every time, and will throw the right
     * exception.
     */
    private synchronized void makeCompositeBuilder()
        throws InvalidObjectException {
      if (compositeBuilder != null) {
        return;
      }

      Class<?> targetClass = (Class<?>) getJavaType();
            /* In this 2D array, each subarray is a set of builders where
               there is no point in consulting the ones after the first if
               the first refuses.  */
      CompositeBuilder[][] builders = {
          {
              new CompositeBuilderViaFrom(targetClass, itemNames),
          },
          {
              new CompositeBuilderViaConstructor(targetClass, itemNames),
          },
          {
              new CompositeBuilderCheckGetters(targetClass, itemNames,
                  getterMappings),
              new CompositeBuilderViaSetters(targetClass, itemNames),
              new CompositeBuilderViaProxy(targetClass, itemNames),
          },
      };
      CompositeBuilder foundBuilder = null;
            /* We try to make a meaningful exception message by
               concatenating each Builder's explanation of why it
               isn't applicable.  */
      final StringBuilder whyNots = new StringBuilder();
      Throwable possibleCause = null;
      find:
      for (CompositeBuilder[] relatedBuilders : builders) {
        for (int i = 0; i < relatedBuilders.length; i++) {
          CompositeBuilder builder = relatedBuilders[i];
          String whyNot = builder.applicable(getters);
          if (whyNot == null) {
            foundBuilder = builder;
            break find;
          }
          Throwable cause = builder.possibleCause();
          if (cause != null) {
            possibleCause = cause;
          }
          if (whyNot.length() > 0) {
            if (whyNots.length() > 0) {
              whyNots.append("; ");
            }
            whyNots.append(whyNot);
            if (i == 0) {
              break; // skip other builders in this group
            }
          }
        }
      }
      if (foundBuilder == null) {
        String msg =
            "Do not know how to make a " + targetClass.getName() +
                " from a CompositeData: " + whyNots;
        if (possibleCause != null) {
          msg += ". Remaining exceptions show a POSSIBLE cause.";
        }
        throw invalidObjectException(msg, possibleCause);
      }
      compositeBuilder = foundBuilder;
    }

    @Override
    public void checkReconstructible() throws InvalidObjectException {
      makeCompositeBuilder();
    }

    @Override
    final Object fromNonNullOpenValue(Object value)
        throws InvalidObjectException {
      makeCompositeBuilder();
      return compositeBuilder.fromCompositeData((CompositeData) value,
          itemNames,
          getterMappings);
    }

    private final String[] itemNames;
    private final Method[] getters;
    private final MXBeanMapping[] getterMappings;
    private CompositeBuilder compositeBuilder;
  }

  /**
   * Converts from a CompositeData to an instance of the targetClass.
   */
  private static abstract class CompositeBuilder {

    CompositeBuilder(Class<?> targetClass, String[] itemNames) {
      this.targetClass = targetClass;
      this.itemNames = itemNames;
    }

    Class<?> getTargetClass() {
      return targetClass;
    }

    String[] getItemNames() {
      return itemNames;
    }

    /**
     * If the subclass is appropriate for targetClass, then the
     * method returns null.  If the subclass is not appropriate,
     * then the method returns an explanation of why not.  If the
     * subclass should be appropriate but there is a problem,
     * then the method throws InvalidObjectException.
     */
    abstract String applicable(Method[] getters)
        throws InvalidObjectException;

    /**
     * If the subclass returns an explanation of why it is not applicable,
     * it can additionally indicate an exception with details.  This is
     * potentially confusing, because the real problem could be that one
     * of the other subclasses is supposed to be applicable but isn't.
     * But the advantage of less information loss probably outweighs the
     * disadvantage of possible confusion.
     */
    Throwable possibleCause() {
      return null;
    }

    abstract Object fromCompositeData(CompositeData cd,
        String[] itemNames,
        MXBeanMapping[] converters)
        throws InvalidObjectException;

    private final Class<?> targetClass;
    private final String[] itemNames;
  }

  /**
   * Builder for when the target class has a method "public static
   * from(CompositeData)".
   */
  private static final class CompositeBuilderViaFrom
      extends CompositeBuilder {

    CompositeBuilderViaFrom(Class<?> targetClass, String[] itemNames) {
      super(targetClass, itemNames);
    }

    String applicable(Method[] getters) throws InvalidObjectException {
      // See if it has a method "T from(CompositeData)"
      // as is conventional for a CompositeDataView
      Class<?> targetClass = getTargetClass();
      try {
        Method fromMethod =
            targetClass.getMethod("from", CompositeData.class);

        if (!Modifier.isStatic(fromMethod.getModifiers())) {
          final String msg =
              "Method from(CompositeData) is not static";
          throw new InvalidObjectException(msg);
        }

        if (fromMethod.getReturnType() != getTargetClass()) {
          final String msg =
              "Method from(CompositeData) returns " +
                  typeName(fromMethod.getReturnType()) +
                  " not " + typeName(targetClass);
          throw new InvalidObjectException(msg);
        }

        this.fromMethod = fromMethod;
        return null; // success!
      } catch (InvalidObjectException e) {
        throw e;
      } catch (Exception e) {
        // OK: it doesn't have the method
        return "no method from(CompositeData)";
      }
    }

    final Object fromCompositeData(CompositeData cd,
        String[] itemNames,
        MXBeanMapping[] converters)
        throws InvalidObjectException {
      try {
        return MethodUtil.invoke(fromMethod, null, new Object[]{cd});
      } catch (Exception e) {
        final String msg = "Failed to invoke from(CompositeData)";
        throw invalidObjectException(msg, e);
      }
    }

    private Method fromMethod;
  }

  /**
   * This builder never actually returns success.  It simply serves
   * to check whether the other builders in the same group have any
   * chance of success.  If any getter in the targetClass returns
   * a type that we don't know how to reconstruct, then we will
   * not be able to make a builder, and there is no point in repeating
   * the error about the problematic getter as many times as there are
   * candidate builders.  Instead, the "applicable" method will return
   * an explanatory string, and the other builders will be skipped.
   * If all the getters are OK, then the "applicable" method will return
   * an empty string and the other builders will be tried.
   */
  private static class CompositeBuilderCheckGetters extends CompositeBuilder {

    CompositeBuilderCheckGetters(Class<?> targetClass, String[] itemNames,
        MXBeanMapping[] getterConverters) {
      super(targetClass, itemNames);
      this.getterConverters = getterConverters;
    }

    String applicable(Method[] getters) {
      for (int i = 0; i < getters.length; i++) {
        try {
          getterConverters[i].checkReconstructible();
        } catch (InvalidObjectException e) {
          possibleCause = e;
          return "method " + getters[i].getName() + " returns type " +
              "that cannot be mapped back from OpenData";
        }
      }
      return "";
    }

    @Override
    Throwable possibleCause() {
      return possibleCause;
    }

    final Object fromCompositeData(CompositeData cd,
        String[] itemNames,
        MXBeanMapping[] converters) {
      throw new Error();
    }

    private final MXBeanMapping[] getterConverters;
    private Throwable possibleCause;
  }

  /**
   * Builder for when the target class has a setter for every getter.
   */
  private static class CompositeBuilderViaSetters extends CompositeBuilder {

    CompositeBuilderViaSetters(Class<?> targetClass, String[] itemNames) {
      super(targetClass, itemNames);
    }

    String applicable(Method[] getters) {
      try {
        Constructor<?> c = getTargetClass().getConstructor();
      } catch (Exception e) {
        return "does not have a public no-arg constructor";
      }

      Method[] setters = new Method[getters.length];
      for (int i = 0; i < getters.length; i++) {
        Method getter = getters[i];
        Class<?> returnType = getter.getReturnType();
        String name = propertyName(getter);
        String setterName = "set" + name;
        Method setter;
        try {
          setter = getTargetClass().getMethod(setterName, returnType);
          if (setter.getReturnType() != void.class) {
            throw new Exception();
          }
        } catch (Exception e) {
          return "not all getters have corresponding setters " +
              "(" + getter + ")";
        }
        setters[i] = setter;
      }
      this.setters = setters;
      return null;
    }

    Object fromCompositeData(CompositeData cd,
        String[] itemNames,
        MXBeanMapping[] converters)
        throws InvalidObjectException {
      Object o;
      try {
        final Class<?> targetClass = getTargetClass();
        ReflectUtil.checkPackageAccess(targetClass);
        o = targetClass.newInstance();
        for (int i = 0; i < itemNames.length; i++) {
          if (cd.containsKey(itemNames[i])) {
            Object openItem = cd.get(itemNames[i]);
            Object javaItem =
                converters[i].fromOpenValue(openItem);
            MethodUtil.invoke(setters[i], o, new Object[]{javaItem});
          }
        }
      } catch (Exception e) {
        throw invalidObjectException(e);
      }
      return o;
    }

    private Method[] setters;
  }

  /**
   * Builder for when the target class has a constructor that is
   * annotated with @ConstructorProperties so we can see the correspondence
   * to getters.
   */
  private static final class CompositeBuilderViaConstructor
      extends CompositeBuilder {

    static class AnnotationHelper {

      private static Class<? extends Annotation> constructorPropertiesClass;
      private static Method valueMethod;

      static {
        findConstructorPropertiesClass();
      }

      @SuppressWarnings("unchecked")
      private static void findConstructorPropertiesClass() {
        try {
          constructorPropertiesClass = (Class<? extends Annotation>)
              Class.forName("java.beans.ConstructorProperties", false,
                  DefaultMXBeanMappingFactory.class.getClassLoader());
          valueMethod = constructorPropertiesClass.getMethod("value");
        } catch (ClassNotFoundException cnf) {
          // java.beans not present
        } catch (NoSuchMethodException e) {
          // should not reach here
          throw new InternalError(e);
        }
      }

      static boolean isAvailable() {
        return constructorPropertiesClass != null;
      }

      static String[] getPropertyNames(Constructor<?> constr) {
        if (!isAvailable()) {
          return null;
        }

        Annotation a = constr.getAnnotation(constructorPropertiesClass);
        if (a == null) {
          return null;
        }

        try {
          return (String[]) valueMethod.invoke(a);
        } catch (InvocationTargetException e) {
          throw new InternalError(e);
        } catch (IllegalAccessException e) {
          throw new InternalError(e);
        }
      }
    }

    CompositeBuilderViaConstructor(Class<?> targetClass, String[] itemNames) {
      super(targetClass, itemNames);
    }

    String applicable(Method[] getters) throws InvalidObjectException {
      if (!AnnotationHelper.isAvailable()) {
        return "@ConstructorProperties annotation not available";
      }

      Class<?> targetClass = getTargetClass();
      Constructor<?>[] constrs = targetClass.getConstructors();

      // Applicable if and only if there are any annotated constructors
      List<Constructor<?>> annotatedConstrList = newList();
      for (Constructor<?> constr : constrs) {
        if (Modifier.isPublic(constr.getModifiers())
            && AnnotationHelper.getPropertyNames(constr) != null) {
          annotatedConstrList.add(constr);
        }
      }

      if (annotatedConstrList.isEmpty()) {
        return "no constructor has @ConstructorProperties annotation";
      }

      annotatedConstructors = newList();

      // Now check that all the annotated constructors are valid
      // and throw an exception if not.

      // First link the itemNames to their getter indexes.
      Map<String, Integer> getterMap = newMap();
      String[] itemNames = getItemNames();
      for (int i = 0; i < itemNames.length; i++) {
        getterMap.put(itemNames[i], i);
      }

      // Run through the constructors making the checks in the spec.
      // For each constructor, remember the correspondence between its
      // parameters and the items.  The int[] for a constructor says
      // what parameter index should get what item.  For example,
      // if element 0 is 2 then that means that item 0 in the
      // CompositeData goes to parameter 2 of the constructor.  If an
      // element is -1, that item isn't given to the constructor.
      // Also remember the set of properties in that constructor
      // so we can test unambiguity.
      Set<BitSet> getterIndexSets = newSet();
      for (Constructor<?> constr : annotatedConstrList) {
        String[] propertyNames = AnnotationHelper.getPropertyNames(constr);

        Type[] paramTypes = constr.getGenericParameterTypes();
        if (paramTypes.length != propertyNames.length) {
          final String msg =
              "Number of constructor params does not match " +
                  "@ConstructorProperties annotation: " + constr;
          throw new InvalidObjectException(msg);
        }

        int[] paramIndexes = new int[getters.length];
        for (int i = 0; i < getters.length; i++) {
          paramIndexes[i] = -1;
        }
        BitSet present = new BitSet();

        for (int i = 0; i < propertyNames.length; i++) {
          String propertyName = propertyNames[i];
          if (!getterMap.containsKey(propertyName)) {
            String msg =
                "@ConstructorProperties includes name " + propertyName +
                    " which does not correspond to a property";
            for (String getterName : getterMap.keySet()) {
              if (getterName.equalsIgnoreCase(propertyName)) {
                msg += " (differs only in case from property " +
                    getterName + ")";
              }
            }
            msg += ": " + constr;
            throw new InvalidObjectException(msg);
          }
          int getterIndex = getterMap.get(propertyName);
          paramIndexes[getterIndex] = i;
          if (present.get(getterIndex)) {
            final String msg =
                "@ConstructorProperties contains property " +
                    propertyName + " more than once: " + constr;
            throw new InvalidObjectException(msg);
          }
          present.set(getterIndex);
          Method getter = getters[getterIndex];
          Type propertyType = getter.getGenericReturnType();
          if (!propertyType.equals(paramTypes[i])) {
            final String msg =
                "@ConstructorProperties gives property " + propertyName +
                    " of type " + propertyType + " for parameter " +
                    " of type " + paramTypes[i] + ": " + constr;
            throw new InvalidObjectException(msg);
          }
        }

        if (!getterIndexSets.add(present)) {
          final String msg =
              "More than one constructor has a @ConstructorProperties " +
                  "annotation with this set of names: " +
                  Arrays.toString(propertyNames);
          throw new InvalidObjectException(msg);
        }

        Constr c = new Constr(constr, paramIndexes, present);
        annotatedConstructors.add(c);
      }

            /* Check that no possible set of items could lead to an ambiguous
             * choice of constructor (spec requires this check).  For any
             * pair of constructors, their union would be the minimal
             * ambiguous set.  If this set itself corresponds to a constructor,
             * there is no ambiguity for that pair.  In the usual case, one
             * of the constructors is a superset of the other so the union is
             * just the bigger constructor.
             *
             * The algorithm here is quadratic in the number of constructors
             * with a @ConstructorProperties annotation.  Typically this corresponds
             * to the number of versions of the class there have been.  Ten
             * would already be a large number, so although it's probably
             * possible to have an O(n lg n) algorithm it wouldn't be
             * worth the complexity.
             */
      for (BitSet a : getterIndexSets) {
        boolean seen = false;
        for (BitSet b : getterIndexSets) {
          if (a == b) {
            seen = true;
          } else if (seen) {
            BitSet u = new BitSet();
            u.or(a);
            u.or(b);
            if (!getterIndexSets.contains(u)) {
              Set<String> names = new TreeSet<String>();
              for (int i = u.nextSetBit(0); i >= 0;
                  i = u.nextSetBit(i + 1)) {
                names.add(itemNames[i]);
              }
              final String msg =
                  "Constructors with @ConstructorProperties annotation " +
                      " would be ambiguous for these items: " +
                      names;
              throw new InvalidObjectException(msg);
            }
          }
        }
      }

      return null; // success!
    }

    final Object fromCompositeData(CompositeData cd,
        String[] itemNames,
        MXBeanMapping[] mappings)
        throws InvalidObjectException {
      // The CompositeData might come from an earlier version where
      // not all the items were present.  We look for a constructor
      // that accepts just the items that are present.  Because of
      // the ambiguity check in applicable(), we know there must be
      // at most one maximally applicable constructor.
      CompositeType ct = cd.getCompositeType();
      BitSet present = new BitSet();
      for (int i = 0; i < itemNames.length; i++) {
        if (ct.getType(itemNames[i]) != null) {
          present.set(i);
        }
      }

      Constr max = null;
      for (Constr constr : annotatedConstructors) {
        if (subset(constr.presentParams, present) &&
            (max == null ||
                subset(max.presentParams, constr.presentParams))) {
          max = constr;
        }
      }

      if (max == null) {
        final String msg =
            "No constructor has a @ConstructorProperties for this set of " +
                "items: " + ct.keySet();
        throw new InvalidObjectException(msg);
      }

      Object[] params = new Object[max.presentParams.cardinality()];
      for (int i = 0; i < itemNames.length; i++) {
        if (!max.presentParams.get(i)) {
          continue;
        }
        Object openItem = cd.get(itemNames[i]);
        Object javaItem = mappings[i].fromOpenValue(openItem);
        int index = max.paramIndexes[i];
        if (index >= 0) {
          params[index] = javaItem;
        }
      }

      try {
        ReflectUtil.checkPackageAccess(max.constructor.getDeclaringClass());
        return max.constructor.newInstance(params);
      } catch (Exception e) {
        final String msg =
            "Exception constructing " + getTargetClass().getName();
        throw invalidObjectException(msg, e);
      }
    }

    private static boolean subset(BitSet sub, BitSet sup) {
      BitSet subcopy = (BitSet) sub.clone();
      subcopy.andNot(sup);
      return subcopy.isEmpty();
    }

    private static class Constr {

      final Constructor<?> constructor;
      final int[] paramIndexes;
      final BitSet presentParams;

      Constr(Constructor<?> constructor, int[] paramIndexes,
          BitSet presentParams) {
        this.constructor = constructor;
        this.paramIndexes = paramIndexes;
        this.presentParams = presentParams;
      }
    }

    private List<Constr> annotatedConstructors;
  }

  /**
   * Builder for when the target class is an interface and contains
   * no methods other than getters.  Then we can make an instance
   * using a dynamic proxy that forwards the getters to the source
   * CompositeData.
   */
  private static final class CompositeBuilderViaProxy
      extends CompositeBuilder {

    CompositeBuilderViaProxy(Class<?> targetClass, String[] itemNames) {
      super(targetClass, itemNames);
    }

    String applicable(Method[] getters) {
      Class<?> targetClass = getTargetClass();
      if (!targetClass.isInterface()) {
        return "not an interface";
      }
      Set<Method> methods =
          newSet(Arrays.asList(targetClass.getMethods()));
      methods.removeAll(Arrays.asList(getters));
            /* If the interface has any methods left over, they better be
             * public methods that are already present in java.lang.Object.
             */
      String bad = null;
      for (Method m : methods) {
        String mname = m.getName();
        Class<?>[] mparams = m.getParameterTypes();
        try {
          Method om = Object.class.getMethod(mname, mparams);
          if (!Modifier.isPublic(om.getModifiers())) {
            bad = mname;
          }
        } catch (NoSuchMethodException e) {
          bad = mname;
        }
                /* We don't catch SecurityException since it shouldn't
                 * happen for a method in Object and if it does we would
                 * like to know about it rather than mysteriously complaining.
                 */
      }
      if (bad != null) {
        return "contains methods other than getters (" + bad + ")";
      }
      return null; // success!
    }

    final Object fromCompositeData(CompositeData cd,
        String[] itemNames,
        MXBeanMapping[] converters) {
      final Class<?> targetClass = getTargetClass();
      return
          Proxy.newProxyInstance(targetClass.getClassLoader(),
              new Class<?>[]{targetClass},
              new CompositeDataInvocationHandler(cd));
    }
  }

  static InvalidObjectException invalidObjectException(String msg,
      Throwable cause) {
    return EnvHelp.initCause(new InvalidObjectException(msg), cause);
  }

  static InvalidObjectException invalidObjectException(Throwable cause) {
    return invalidObjectException(cause.getMessage(), cause);
  }

  static OpenDataException openDataException(String msg, Throwable cause) {
    return EnvHelp.initCause(new OpenDataException(msg), cause);
  }

  static OpenDataException openDataException(Throwable cause) {
    return openDataException(cause.getMessage(), cause);
  }

  static void mustBeComparable(Class<?> collection, Type element)
      throws OpenDataException {
    if (!(element instanceof Class<?>)
        || !Comparable.class.isAssignableFrom((Class<?>) element)) {
      final String msg =
          "Parameter class " + element + " of " +
              collection.getName() + " does not implement " +
              Comparable.class.getName();
      throw new OpenDataException(msg);
    }
  }

  /**
   * Utility method to take a string and convert it to normal Java variable
   * name capitalization.  This normally means converting the first
   * character from upper case to lower case, but in the (unusual) special
   * case when there is more than one character and both the first and
   * second characters are upper case, we leave it alone.
   * <p>
   * Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays
   * as "URL".
   *
   * @param name The string to be decapitalized.
   * @return The decapitalized version of the string.
   */
  public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
      return name;
    }
    int offset1 = Character.offsetByCodePoints(name, 0, 1);
    // Should be name.offsetByCodePoints but 6242664 makes this fail
    if (offset1 < name.length() &&
        Character.isUpperCase(name.codePointAt(offset1))) {
      return name;
    }
    return name.substring(0, offset1).toLowerCase() +
        name.substring(offset1);
  }

  /**
   * Reverse operation for java.beans.Introspector.decapitalize.  For any s,
   * capitalize(decapitalize(s)).equals(s).  The reverse is not true:
   * e.g. capitalize("uRL") produces "URL" which is unchanged by
   * decapitalize.
   */
  static String capitalize(String name) {
    if (name == null || name.length() == 0) {
      return name;
    }
    int offset1 = name.offsetByCodePoints(0, 1);
    return name.substring(0, offset1).toUpperCase() +
        name.substring(offset1);
  }

  public static String propertyName(Method m) {
    String rest = null;
    String name = m.getName();
    if (name.startsWith("get")) {
      rest = name.substring(3);
    } else if (name.startsWith("is") && m.getReturnType() == boolean.class) {
      rest = name.substring(2);
    }
    if (rest == null || rest.length() == 0
        || m.getParameterTypes().length > 0
        || m.getReturnType() == void.class
        || name.equals("getClass")) {
      return null;
    }
    return rest;
  }

  private final static Map<Type, Type> inProgress = newIdentityHashMap();
  // really an IdentityHashSet but that doesn't exist
}
